<?php
/**
 * @author: xingshenqiang<shenqiang.xing@vhall.com>
 * @date:   2022-07-28
 */

namespace Vhall\ErrorHandler;

use Closure;
use InvalidArgumentException;
use Symfony\Component\Console\Application as ConsoleApplication;
use Symfony\Component\Console\Output\ConsoleOutput;
use Throwable;

class Handler implements ExceptionHandler
{
    /**
     * A list of the exception types that are not reported.
     *
     * @var string[]
     */
    protected $dontReport = [];

    /**
     * A list of the internal exception types that should not be reported.
     *
     * @var string[]
     */
    protected $internalDontReport = [
    ];

    /**
     * The registered exception mappings.
     *
     * @var array<string, Closure>
     */
    protected $exceptionMap = [];

    public function __construct()
    {
        $this->register();
    }

    /**
     * Register the exception handling callbacks for the application.
     *
     * @return void
     */
    public function register()
    {
        //
    }

    public function report(Throwable $e): void
    {
        $e = $this->mapException($e);

        if ($this->shouldntReport($e)) {
            return;
        }

        method_exists($e, 'report') && $e->report();
    }

    /**
     * Determine if the exception should be reported.
     *
     * @param Throwable $e
     * @return bool
     */
    public function shouldReport(Throwable $e): bool
    {
        return ! $this->shouldntReport($e);
    }

    /**
     * Map the exception using a registered mapper if possible.
     *
     * @param Throwable $e
     * @return Throwable
     */
    protected function mapException(Throwable $e)
    {
        foreach ($this->exceptionMap as $class => $mapper) {
            if (is_a($e, $class)) {
                return $mapper($e);
            }
        }

        return $e;
    }

    /**
     * Determine if the exception is in the "do not report" list.
     *
     * @param Throwable $e
     * @return bool
     */
    protected function shouldntReport(Throwable $e): bool
    {
        return !empty(array_filter(array_merge($this->dontReport, $this->internalDontReport),
            static function ($type) use ($e) {
                return $e instanceof $type;
            })
        );

    }

    public function renderForConsole(Throwable $e): bool
    {
        if (class_exists(ConsoleOutput::class) && class_exists(ConsoleApplication::class)) {
            (new ConsoleApplication)->renderThrowable($e, new ConsoleOutput);
            return true;
        }
        return false;
    }

    public function renderHttpResponse(Throwable $e): bool
    {
        if (method_exists($e, 'render')) {
            return false !== $e->render();
        }
        return false;
    }


    /**
     * Register a new exception mapping.
     *
     * @param Closure|string      $from
     * @param Closure|string|null $to
     * @return $this
     *
     * @throws InvalidArgumentException
     */
    public function map($from, $to = null)
    {
        if (is_string($to)) {
            $to = static function ($exception) use ($to) {
                return new $to('', 0, $exception);
            };
        }

        if (! is_string($from) || ! $to instanceof Closure) {
            throw new InvalidArgumentException('Invalid exception mapping.');
        }

        $this->exceptionMap[$from] = $to;

        return $this;
    }
}